/**
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0.
 */

#include <aws/cal/cal.h>
#include <aws/common/allocator.h>
#include <aws/common/logging.h>
#include <aws/common/mutex.h>
#include <aws/common/thread.h>

#include <dlfcn.h>

#include <aws/cal/private/opensslcrypto_common.h>

/*
 * OpenSSL 3 has a large amount of interface changes and many of the functions used
 * throughout aws-c-cal have become deprecated.
 * Lets disable deprecation warnings, so that we can atleast run CI, until we
 * can move over to new functions.
 */
#define OPENSSL_SUPPRESS_DEPRECATED
#include <openssl/crypto.h>

#if defined(OPENSSL_IS_AWSLC)
#    include <openssl/service_indicator.h>
#endif

static struct openssl_hmac_ctx_table hmac_ctx_table;
static struct openssl_evp_md_ctx_table evp_md_ctx_table;

struct openssl_hmac_ctx_table *g_aws_openssl_hmac_ctx_table = NULL;
struct openssl_evp_md_ctx_table *g_aws_openssl_evp_md_ctx_table = NULL;

static struct aws_allocator *s_libcrypto_allocator = NULL;

/* weak refs to libcrypto functions to force them to at least try to link
 * and avoid dead-stripping
 */
#if defined(OPENSSL_IS_AWSLC) || defined(OPENSSL_IS_BORINGSSL)
/* TODO:the weak refs is not GUARANTEED to avoid linker to strip the symbol.
   Build on musl with openssl 1.1.1w, those those was referenced, but still stripped from libcrypto during linking.
   Logs was:
   ```
   / # gcc -static -Wl,--trace-symbol=HMAC_CTX_new,-v -o test test.c -I $HOME/opt/aws/include -L $HOME/opt/aws/lib
   -laws-c-cal -laws-c-common /usr/lib/libcrypto.a -fno-lto collect2 version 11.2.1 20220219
    /usr/lib/gcc/x86_64-alpine-linux-musl/11.2.1/../../../../x86_64-alpine-linux-musl/bin/ld --hash-style=gnu -m
   elf_x86_64 --as-needed -static -z now -o test /usr/lib/gcc/x86_64-alpine-linux-musl/11.2.1/../../../../lib/crt1.o
   /usr/lib/gcc/x86_64-alpine-linux-musl/11.2.1/../../../../lib/crti.o
   /usr/lib/gcc/x86_64-alpine-linux-musl/11.2.1/crtbeginT.o -L/root/opt/aws/lib
   -L/usr/lib/gcc/x86_64-alpine-linux-musl/11.2.1
   -L/usr/lib/gcc/x86_64-alpine-linux-musl/11.2.1/../../../../x86_64-alpine-linux-musl/lib/../lib
   -L/usr/lib/gcc/x86_64-alpine-linux-musl/11.2.1/../../../../lib -L/lib/../lib -L/usr/lib/../lib
   -L/usr/lib/gcc/x86_64-alpine-linux-musl/11.2.1/../../../../x86_64-alpine-linux-musl/lib
   -L/usr/lib/gcc/x86_64-alpine-linux-musl/11.2.1/../../.. --trace-symbol=HMAC_CTX_new -v /tmp/ccBcKMkh.o -laws-c-cal
   -laws-c-common /usr/lib/libcrypto.a -lssp_nonshared --start-group -lgcc -lgcc_eh -lc --end-group
   /usr/lib/gcc/x86_64-alpine-linux-musl/11.2.1/crtend.o
   /usr/lib/gcc/x86_64-alpine-linux-musl/11.2.1/../../../../lib/crtn.o

   GNU ld (GNU Binutils) 2.38
   /usr/lib/gcc/x86_64-alpine-linux-musl/11.2.1/../../../../x86_64-alpine-linux-musl/bin/ld:
   /root/opt/aws/lib/libaws-c-cal.a(openssl_platform_init.c.o): reference to HMAC_CTX_new
   ```
    We don't really understand what strips the symbols, but aws-lc is a workaround.
    And, --require-defined=HMAC_CTX_new is another workaround to force the linker to keep the symbol.
*/
extern HMAC_CTX *HMAC_CTX_new(void) __attribute__((weak, used));
extern void HMAC_CTX_free(HMAC_CTX *) __attribute__((weak, used));
extern void HMAC_CTX_init(HMAC_CTX *) __attribute__((weak, used));
extern void HMAC_CTX_cleanup(HMAC_CTX *) __attribute__((weak, used));
extern int HMAC_Update(HMAC_CTX *, const unsigned char *, size_t) __attribute__((weak, used));
extern int HMAC_Final(HMAC_CTX *, unsigned char *, unsigned int *) __attribute__((weak, used));
extern int HMAC_Init_ex(HMAC_CTX *, const void *, size_t, const EVP_MD *, ENGINE *) __attribute__((weak, used));

static int s_hmac_init_ex_bssl(HMAC_CTX *ctx, const void *key, size_t key_len, const EVP_MD *md, ENGINE *impl) {
    AWS_PRECONDITION(ctx);

    int (*init_ex_pt)(HMAC_CTX *, const void *, size_t, const EVP_MD *, ENGINE *) = (int (*)(
        HMAC_CTX *, const void *, size_t, const EVP_MD *, ENGINE *))g_aws_openssl_hmac_ctx_table->impl.init_ex_fn;

    return init_ex_pt(ctx, key, key_len, md, impl);
}

#else
/* 1.1 */
extern HMAC_CTX *HMAC_CTX_new(void) __attribute__((weak, used));
extern void HMAC_CTX_free(HMAC_CTX *) __attribute__((weak, used));

/* 1.0.2 */
extern void HMAC_CTX_init(HMAC_CTX *) __attribute__((weak, used));
extern void HMAC_CTX_cleanup(HMAC_CTX *) __attribute__((weak, used));

/* common */
extern int HMAC_Update(HMAC_CTX *, const unsigned char *, size_t) __attribute__((weak, used));
extern int HMAC_Final(HMAC_CTX *, unsigned char *, unsigned int *) __attribute__((weak, used));
extern int HMAC_Init_ex(HMAC_CTX *, const void *, int, const EVP_MD *, ENGINE *) __attribute__((weak, used));

static int s_hmac_init_ex_openssl(HMAC_CTX *ctx, const void *key, size_t key_len, const EVP_MD *md, ENGINE *impl) {
    AWS_PRECONDITION(ctx);
    if (key_len > INT_MAX) {
        return 0;
    }

    /*Note: unlike aws-lc and boringssl, openssl 1.1.1 and 1.0.2 take int as key
    len arg. */
    int (*init_ex_ptr)(HMAC_CTX *, const void *, int, const EVP_MD *, ENGINE *) =
        (int (*)(HMAC_CTX *, const void *, int, const EVP_MD *, ENGINE *))g_aws_openssl_hmac_ctx_table->impl.init_ex_fn;

    return init_ex_ptr(ctx, key, (int)key_len, md, impl);
}

#endif /* !OPENSSL_IS_AWSLC && !OPENSSL_IS_BORINGSSL*/

#if !defined(OPENSSL_IS_AWSLC)
/* libcrypto 1.1 stub for init */
static void s_hmac_ctx_init_noop(HMAC_CTX *ctx) {
    (void)ctx;
}

/* libcrypto 1.1 stub for clean_up */
static void s_hmac_ctx_clean_up_noop(HMAC_CTX *ctx) {
    (void)ctx;
}
#endif

#if defined(OPENSSL_IS_OPENSSL)
/* libcrypto 1.0 shim for new */
static HMAC_CTX *s_hmac_ctx_new(void) {
    AWS_PRECONDITION(
        g_aws_openssl_hmac_ctx_table->init_fn != s_hmac_ctx_init_noop &&
        "libcrypto 1.0 init called on libcrypto 1.1 vtable");
    HMAC_CTX *ctx = aws_mem_calloc(s_libcrypto_allocator, 1, 300);
    AWS_FATAL_ASSERT(ctx && "Unable to allocate to HMAC_CTX");
    g_aws_openssl_hmac_ctx_table->init_fn(ctx);
    return ctx;
}

/* libcrypto 1.0 shim for free */
static void s_hmac_ctx_free(HMAC_CTX *ctx) {
    AWS_PRECONDITION(ctx);
    AWS_PRECONDITION(
        g_aws_openssl_hmac_ctx_table->clean_up_fn != s_hmac_ctx_clean_up_noop &&
        "libcrypto 1.0 clean_up called on libcrypto 1.1 vtable");
    g_aws_openssl_hmac_ctx_table->clean_up_fn(ctx);
    aws_mem_release(s_libcrypto_allocator, ctx);
}

#endif /* !OPENSSL_IS_AWSLC */

enum aws_libcrypto_version {
    AWS_LIBCRYPTO_NONE = 0,
    AWS_LIBCRYPTO_1_0_2,
    AWS_LIBCRYPTO_1_1_1,
    AWS_LIBCRYPTO_LC,
    AWS_LIBCRYPTO_BORINGSSL
};

bool s_resolve_hmac_102(void *module) {
#if defined(OPENSSL_IS_OPENSSL)
    hmac_ctx_init init_fn = (hmac_ctx_init)HMAC_CTX_init;
    hmac_ctx_clean_up clean_up_fn = (hmac_ctx_clean_up)HMAC_CTX_cleanup;
    hmac_update update_fn = (hmac_update)HMAC_Update;
    hmac_final final_fn = (hmac_final)HMAC_Final;
    hmac_init_ex init_ex_fn = (hmac_init_ex)HMAC_Init_ex;

    /* were symbols bound by static linking? */
    bool has_102_symbols = init_fn && clean_up_fn && update_fn && final_fn && init_ex_fn;
    if (has_102_symbols) {
        AWS_LOGF_DEBUG(AWS_LS_CAL_LIBCRYPTO_RESOLVE, "found weak ref libcrypto 1.0.2 HMAC symbols");
    } else {
        if (!module) {
            AWS_LOGF_DEBUG(AWS_LS_CAL_LIBCRYPTO_RESOLVE, "No loaded libcrypto 1.0.2 HMAC symbols found");
            return false;
        }
        /* If symbols aren't already found, try to find the requested version */
        *(void **)(&init_fn) = dlsym(module, "HMAC_CTX_init");
        *(void **)(&clean_up_fn) = dlsym(module, "HMAC_CTX_cleanup");
        *(void **)(&update_fn) = dlsym(module, "HMAC_Update");
        *(void **)(&final_fn) = dlsym(module, "HMAC_Final");
        *(void **)(&init_ex_fn) = dlsym(module, "HMAC_Init_ex");
        if (init_fn) {
            AWS_LOGF_DEBUG(AWS_LS_CAL_LIBCRYPTO_RESOLVE, "found dynamic libcrypto 1.0.2 HMAC symbols");
        }
    }

    if (init_fn) {
        hmac_ctx_table.new_fn = (hmac_ctx_new)s_hmac_ctx_new;
        hmac_ctx_table.free_fn = s_hmac_ctx_free;
        hmac_ctx_table.init_fn = init_fn;
        hmac_ctx_table.clean_up_fn = clean_up_fn;
        hmac_ctx_table.update_fn = update_fn;
        hmac_ctx_table.final_fn = final_fn;
        hmac_ctx_table.init_ex_fn = init_ex_fn;
        g_aws_openssl_hmac_ctx_table = &hmac_ctx_table;
        return true;
    }
#endif
    return false;
}

bool s_resolve_hmac_111(void *module) {
#if defined(OPENSSL_IS_OPENSSL)
    hmac_ctx_new new_fn = (hmac_ctx_new)HMAC_CTX_new;
    hmac_ctx_free free_fn = (hmac_ctx_free)HMAC_CTX_free;
    hmac_update update_fn = (hmac_update)HMAC_Update;
    hmac_final final_fn = (hmac_final)HMAC_Final;
    hmac_init_ex init_ex_fn = (hmac_init_ex)HMAC_Init_ex;

    /* were symbols bound by static linking? */
    bool has_111_symbols = new_fn && free_fn && update_fn && final_fn && init_ex_fn;

    if (has_111_symbols) {
        AWS_LOGF_DEBUG(AWS_LS_CAL_LIBCRYPTO_RESOLVE, "found weak ref libcrypto 1.1.1 HMAC symbols");
    } else {
        if (!module) {
            AWS_LOGF_DEBUG(AWS_LS_CAL_LIBCRYPTO_RESOLVE, "No loaded libcrypto 1.1.1 HMAC symbols found");
            return false;
        }
        *(void **)(&new_fn) = dlsym(module, "HMAC_CTX_new");
        *(void **)(&free_fn) = dlsym(module, "HMAC_CTX_free");
        *(void **)(&update_fn) = dlsym(module, "HMAC_Update");
        *(void **)(&final_fn) = dlsym(module, "HMAC_Final");
        *(void **)(&init_ex_fn) = dlsym(module, "HMAC_Init_ex");
        if (new_fn) {
            AWS_LOGF_DEBUG(AWS_LS_CAL_LIBCRYPTO_RESOLVE, "found dynamic libcrypto 1.1.1 HMAC symbols");
        }
    }

    if (new_fn) {
        hmac_ctx_table.new_fn = new_fn;
        hmac_ctx_table.free_fn = free_fn;
        hmac_ctx_table.init_fn = s_hmac_ctx_init_noop;
        hmac_ctx_table.clean_up_fn = s_hmac_ctx_clean_up_noop;
        hmac_ctx_table.update_fn = update_fn;
        hmac_ctx_table.final_fn = final_fn;
        hmac_ctx_table.init_ex_fn = s_hmac_init_ex_openssl;
        hmac_ctx_table.impl.init_ex_fn = (crypto_generic_fn_ptr)init_ex_fn;
        g_aws_openssl_hmac_ctx_table = &hmac_ctx_table;
        return true;
    }
#endif
    return false;
}

bool s_resolve_hmac_lc(void *module) {
#if defined(OPENSSL_IS_AWSLC)
    hmac_ctx_init init_fn = (hmac_ctx_init)HMAC_CTX_init;
    hmac_ctx_clean_up clean_up_fn = (hmac_ctx_clean_up)HMAC_CTX_cleanup;
    hmac_ctx_new new_fn = (hmac_ctx_new)HMAC_CTX_new;
    hmac_ctx_free free_fn = (hmac_ctx_free)HMAC_CTX_free;
    hmac_update update_fn = (hmac_update)HMAC_Update;
    hmac_final final_fn = (hmac_final)HMAC_Final;
    hmac_init_ex init_ex_fn = (hmac_init_ex)HMAC_Init_ex;

    /* were symbols bound by static linking? */
    bool has_awslc_symbols = new_fn && free_fn && update_fn && final_fn && init_fn && init_ex_fn;

    /* If symbols aren't already found, try to find the requested version */
    /* when built as a shared lib, and multiple versions of libcrypto are possibly
     * available (e.g. brazil), select AWS-LC by default for consistency */
    if (has_awslc_symbols) {
        AWS_LOGF_DEBUG(AWS_LS_CAL_LIBCRYPTO_RESOLVE, "found weak ref aws-lc HMAC symbols");
    } else {
        if (!module) {
            AWS_LOGF_DEBUG(AWS_LS_CAL_LIBCRYPTO_RESOLVE, "No loaded aws-lc HMAC symbols found");
            return false;
        }
        *(void **)(&new_fn) = dlsym(module, "HMAC_CTX_new");
        *(void **)(&free_fn) = dlsym(module, "HMAC_CTX_free");
        *(void **)(&update_fn) = dlsym(module, "HMAC_Update");
        *(void **)(&final_fn) = dlsym(module, "HMAC_Final");
        *(void **)(&init_ex_fn) = dlsym(module, "HMAC_Init_ex");
        if (new_fn) {
            AWS_LOGF_DEBUG(AWS_LS_CAL_LIBCRYPTO_RESOLVE, "found dynamic aws-lc HMAC symbols");
        }
    }

    if (new_fn) {
        /* Fill out the vtable for the requested version */
        hmac_ctx_table.new_fn = new_fn;
        hmac_ctx_table.free_fn = free_fn;
        hmac_ctx_table.init_fn = init_fn;
        hmac_ctx_table.clean_up_fn = clean_up_fn;
        hmac_ctx_table.update_fn = update_fn;
        hmac_ctx_table.final_fn = final_fn;
        hmac_ctx_table.init_ex_fn = s_hmac_init_ex_bssl;
        hmac_ctx_table.impl.init_ex_fn = (crypto_generic_fn_ptr)init_ex_fn;
        g_aws_openssl_hmac_ctx_table = &hmac_ctx_table;
        return true;
    }
#endif
    return false;
}

bool s_resolve_hmac_boringssl(void *module) {
#if defined(OPENSSL_IS_BORINGSSL)
    hmac_ctx_new new_fn = (hmac_ctx_new)HMAC_CTX_new;
    hmac_ctx_free free_fn = (hmac_ctx_free)HMAC_CTX_free;
    hmac_update update_fn = (hmac_update)HMAC_Update;
    hmac_final final_fn = (hmac_final)HMAC_Final;
    hmac_init_ex init_ex_fn = (hmac_init_ex)HMAC_Init_ex;

    /* were symbols bound by static linking? */
    bool has_bssl_symbols = new_fn && free_fn && update_fn && final_fn && init_ex_fn;

    if (has_bssl_symbols) {
        AWS_LOGF_DEBUG(AWS_LS_CAL_LIBCRYPTO_RESOLVE, "found weak ref boringssl HMAC symbols");
    } else {
        if (!module) {
            AWS_LOGF_DEBUG(AWS_LS_CAL_LIBCRYPTO_RESOLVE, "No loaded boringssl HMAC symbols found");
            return false;
        }
        *(void **)(&new_fn) = dlsym(module, "HMAC_CTX_new");
        *(void **)(&free_fn) = dlsym(module, "HMAC_CTX_free");
        *(void **)(&update_fn) = dlsym(module, "HMAC_Update");
        *(void **)(&final_fn) = dlsym(module, "HMAC_Final");
        *(void **)(&init_ex_fn) = dlsym(module, "HMAC_Init_ex");
        if (new_fn) {
            AWS_LOGF_DEBUG(AWS_LS_CAL_LIBCRYPTO_RESOLVE, "found dynamic boringssl HMAC symbols");
        }
    }

    if (new_fn) {
        hmac_ctx_table.new_fn = new_fn;
        hmac_ctx_table.free_fn = free_fn;
        hmac_ctx_table.init_fn = s_hmac_ctx_init_noop;
        hmac_ctx_table.clean_up_fn = s_hmac_ctx_clean_up_noop;
        hmac_ctx_table.update_fn = update_fn;
        hmac_ctx_table.final_fn = final_fn;
        hmac_ctx_table.init_ex_fn = s_hmac_init_ex_bssl;
        hmac_ctx_table.impl.init_ex_fn = (crypto_generic_fn_ptr)init_ex_fn;
        g_aws_openssl_hmac_ctx_table = &hmac_ctx_table;
        return true;
    }
#endif
    return false;
}

static enum aws_libcrypto_version s_resolve_libcrypto_hmac(enum aws_libcrypto_version version, void *module) {
    switch (version) {
        case AWS_LIBCRYPTO_LC:
            return s_resolve_hmac_lc(module) ? version : AWS_LIBCRYPTO_NONE;
        case AWS_LIBCRYPTO_1_1_1:
            return s_resolve_hmac_111(module) ? version : AWS_LIBCRYPTO_NONE;
        case AWS_LIBCRYPTO_1_0_2:
            return s_resolve_hmac_102(module) ? version : AWS_LIBCRYPTO_NONE;
        case AWS_LIBCRYPTO_BORINGSSL:
            return s_resolve_hmac_boringssl(module) ? version : AWS_LIBCRYPTO_NONE;
        case AWS_LIBCRYPTO_NONE:
            AWS_FATAL_ASSERT(!"Attempted to resolve invalid libcrypto HMAC API version AWS_LIBCRYPTO_NONE");
    }

    return AWS_LIBCRYPTO_NONE;
}

#if !defined(OPENSSL_IS_AWSLC)
/* EVP_MD_CTX API */
/* 1.0.2 NOTE: these are macros in 1.1.x, so we have to undef them to weak link */
#    if defined(EVP_MD_CTX_create)
#        pragma push_macro("EVP_MD_CTX_create")
#        undef EVP_MD_CTX_create
#    endif
extern EVP_MD_CTX *EVP_MD_CTX_create(void) __attribute__((weak, used));
static evp_md_ctx_new s_EVP_MD_CTX_create = EVP_MD_CTX_create;
#    if defined(EVP_MD_CTX_create)
#        pragma pop_macro("EVP_MD_CTX_create")
#    endif

#    if defined(EVP_MD_CTX_destroy)
#        pragma push_macro("EVP_MD_CTX_destroy")
#        undef EVP_MD_CTX_destroy
#    endif
extern void EVP_MD_CTX_destroy(EVP_MD_CTX *) __attribute__((weak, used));
static evp_md_ctx_free s_EVP_MD_CTX_destroy = EVP_MD_CTX_destroy;
#    if defined(EVP_MD_CTX_destroy)
#        pragma pop_macro("EVP_MD_CTX_destroy")
#    endif
#endif /* !OPENSSL_IS_AWSLC */

extern EVP_MD_CTX *EVP_MD_CTX_new(void) __attribute__((weak, used));
extern void EVP_MD_CTX_free(EVP_MD_CTX *) __attribute__((weak, used));
extern int EVP_DigestInit_ex(EVP_MD_CTX *, const EVP_MD *, ENGINE *) __attribute__((weak, used));
extern int EVP_DigestUpdate(EVP_MD_CTX *, const void *, size_t) __attribute__((weak, used));
extern int EVP_DigestFinal_ex(EVP_MD_CTX *, unsigned char *, unsigned int *) __attribute__((weak, used));

bool s_resolve_md_102(void *module) {
#if !defined(OPENSSL_IS_AWSLC)
    evp_md_ctx_new md_create_fn = s_EVP_MD_CTX_create;
    evp_md_ctx_free md_destroy_fn = s_EVP_MD_CTX_destroy;
    evp_md_ctx_digest_init_ex md_init_ex_fn = EVP_DigestInit_ex;
    evp_md_ctx_digest_update md_update_fn = EVP_DigestUpdate;
    evp_md_ctx_digest_final_ex md_final_ex_fn = EVP_DigestFinal_ex;

    bool has_102_symbols = md_create_fn && md_destroy_fn && md_init_ex_fn && md_update_fn && md_final_ex_fn;

    if (has_102_symbols) {
        AWS_LOGF_DEBUG(AWS_LS_CAL_LIBCRYPTO_RESOLVE, "found weak ref libcrypto 1.0.2 EVP_MD symbols");
    } else {
        if (!module) {
            AWS_LOGF_DEBUG(AWS_LS_CAL_LIBCRYPTO_RESOLVE, "No loaded libcrypto 1.0.2 EVP_MD symbols found");
            return false;
        }
        *(void **)(&md_create_fn) = dlsym(module, "EVP_MD_CTX_create");
        *(void **)(&md_destroy_fn) = dlsym(module, "EVP_MD_CTX_destroy");
        *(void **)(&md_init_ex_fn) = dlsym(module, "EVP_DigestInit_ex");
        *(void **)(&md_update_fn) = dlsym(module, "EVP_DigestUpdate");
        *(void **)(&md_final_ex_fn) = dlsym(module, "EVP_DigestFinal_ex");
        if (md_create_fn) {
            AWS_LOGF_DEBUG(AWS_LS_CAL_LIBCRYPTO_RESOLVE, "found dynamic libcrypto 1.0.2 EVP_MD symbols");
        }
    }

    if (md_create_fn) {
        evp_md_ctx_table.new_fn = md_create_fn;
        evp_md_ctx_table.free_fn = md_destroy_fn;
        evp_md_ctx_table.init_ex_fn = md_init_ex_fn;
        evp_md_ctx_table.update_fn = md_update_fn;
        evp_md_ctx_table.final_ex_fn = md_final_ex_fn;
        g_aws_openssl_evp_md_ctx_table = &evp_md_ctx_table;
        return true;
    }
#endif
    return false;
}

bool s_resolve_md_111(void *module) {
#if !defined(OPENSSL_IS_AWSLC)
    evp_md_ctx_new md_new_fn = EVP_MD_CTX_new;
    evp_md_ctx_free md_free_fn = EVP_MD_CTX_free;
    evp_md_ctx_digest_init_ex md_init_ex_fn = EVP_DigestInit_ex;
    evp_md_ctx_digest_update md_update_fn = EVP_DigestUpdate;
    evp_md_ctx_digest_final_ex md_final_ex_fn = EVP_DigestFinal_ex;

    bool has_111_symbols = md_new_fn && md_free_fn && md_init_ex_fn && md_update_fn && md_final_ex_fn;
    if (has_111_symbols) {
        AWS_LOGF_DEBUG(AWS_LS_CAL_LIBCRYPTO_RESOLVE, "found weak ref libcrypto 1.1.1 EVP_MD symbols");
    } else {
        if (!module) {
            AWS_LOGF_DEBUG(AWS_LS_CAL_LIBCRYPTO_RESOLVE, "No loaded libcrypto 1.1.1 EVP_MD symbols found");
            return false;
        }
        *(void **)(&md_new_fn) = dlsym(module, "EVP_MD_CTX_new");
        *(void **)(&md_free_fn) = dlsym(module, "EVP_MD_CTX_free");
        *(void **)(&md_init_ex_fn) = dlsym(module, "EVP_DigestInit_ex");
        *(void **)(&md_update_fn) = dlsym(module, "EVP_DigestUpdate");
        *(void **)(&md_final_ex_fn) = dlsym(module, "EVP_DigestFinal_ex");
        if (md_new_fn) {
            AWS_LOGF_DEBUG(AWS_LS_CAL_LIBCRYPTO_RESOLVE, "found dynamic libcrypto 1.1.1 EVP_MD symbols");
        }
    }

    if (md_new_fn) {
        evp_md_ctx_table.new_fn = md_new_fn;
        evp_md_ctx_table.free_fn = md_free_fn;
        evp_md_ctx_table.init_ex_fn = md_init_ex_fn;
        evp_md_ctx_table.update_fn = md_update_fn;
        evp_md_ctx_table.final_ex_fn = md_final_ex_fn;
        g_aws_openssl_evp_md_ctx_table = &evp_md_ctx_table;
        return true;
    }
#endif
    return false;
}

bool s_resolve_md_lc(void *module) {
#if defined(OPENSSL_IS_AWSLC)
    evp_md_ctx_new md_new_fn = EVP_MD_CTX_new;
    evp_md_ctx_new md_create_fn = EVP_MD_CTX_new;
    evp_md_ctx_free md_free_fn = EVP_MD_CTX_free;
    evp_md_ctx_free md_destroy_fn = EVP_MD_CTX_destroy;
    evp_md_ctx_digest_init_ex md_init_ex_fn = EVP_DigestInit_ex;
    evp_md_ctx_digest_update md_update_fn = EVP_DigestUpdate;
    evp_md_ctx_digest_final_ex md_final_ex_fn = EVP_DigestFinal_ex;

    bool has_awslc_symbols =
        md_new_fn && md_create_fn && md_free_fn && md_destroy_fn && md_init_ex_fn && md_update_fn && md_final_ex_fn;

    if (has_awslc_symbols) {
        AWS_LOGF_DEBUG(AWS_LS_CAL_LIBCRYPTO_RESOLVE, "found weak ref aws-lc libcrypto 1.1.1 EVP_MD symbols");
    } else {
        if (!module) {
            AWS_LOGF_DEBUG(AWS_LS_CAL_LIBCRYPTO_RESOLVE, "No loaded aws-lc libcrypto 1.1.1 EVP_MD symbols found");
            return false;
        }
        *(void **)(&md_new_fn) = dlsym(module, "EVP_MD_CTX_new");
        *(void **)(&md_free_fn) = dlsym(module, "EVP_MD_CTX_free");
        *(void **)(&md_init_ex_fn) = dlsym(module, "EVP_DigestInit_ex");
        *(void **)(&md_update_fn) = dlsym(module, "EVP_DigestUpdate");
        *(void **)(&md_final_ex_fn) = dlsym(module, "EVP_DigestFinal_ex");
        if (md_new_fn) {
            AWS_LOGF_DEBUG(AWS_LS_CAL_LIBCRYPTO_RESOLVE, "found dynamic aws-lc libcrypto 1.1.1 EVP_MD symbols");
        }
    }

    if (md_new_fn) {
        /* Add the found symbols to the vtable */
        evp_md_ctx_table.new_fn = md_new_fn;
        evp_md_ctx_table.free_fn = md_free_fn;
        evp_md_ctx_table.init_ex_fn = md_init_ex_fn;
        evp_md_ctx_table.update_fn = md_update_fn;
        evp_md_ctx_table.final_ex_fn = md_final_ex_fn;
        g_aws_openssl_evp_md_ctx_table = &evp_md_ctx_table;
        return true;
    }
#endif
    return false;
}

bool s_resolve_md_boringssl(void *module) {
#if !defined(OPENSSL_IS_AWSLC)
    return s_resolve_md_111(module);
#else
    return false;
#endif
}

static enum aws_libcrypto_version s_resolve_libcrypto_md(enum aws_libcrypto_version version, void *module) {
    switch (version) {
        case AWS_LIBCRYPTO_LC:
            return s_resolve_md_lc(module) ? version : AWS_LIBCRYPTO_NONE;
        case AWS_LIBCRYPTO_1_1_1:
            return s_resolve_md_111(module) ? version : AWS_LIBCRYPTO_NONE;
        case AWS_LIBCRYPTO_1_0_2:
            return s_resolve_md_102(module) ? version : AWS_LIBCRYPTO_NONE;
        case AWS_LIBCRYPTO_BORINGSSL:
            return s_resolve_md_boringssl(module) ? version : AWS_LIBCRYPTO_NONE;
        case AWS_LIBCRYPTO_NONE:
            AWS_FATAL_ASSERT(!"Attempted to resolve invalid libcrypto MD API version AWS_LIBCRYPTO_NONE");
    }

    return AWS_LIBCRYPTO_NONE;
}

static enum aws_libcrypto_version s_resolve_libcrypto_symbols(enum aws_libcrypto_version version, void *module) {
    enum aws_libcrypto_version found_version = s_resolve_libcrypto_hmac(version, module);
    if (found_version == AWS_LIBCRYPTO_NONE) {
        return AWS_LIBCRYPTO_NONE;
    }
    found_version = s_resolve_libcrypto_md(found_version, module);
    if (found_version == AWS_LIBCRYPTO_NONE) {
        return AWS_LIBCRYPTO_NONE;
    }
    return found_version;
}

static enum aws_libcrypto_version s_libcrypto_version_at_compile_time(void) {
#ifdef OPENSSL_IS_OPENSSL
    /*
     * Currently, this only checks for 1.0.2 vs 1.1. As a future optimization, we can also add a branch for OpenSSL 3.0.
     * OpenSSL 3.0 is compatible with OpenSSL 1.1, so it works currently.
     */
    if (OPENSSL_VERSION_NUMBER < 0x10100000L) {
        return AWS_LIBCRYPTO_1_0_2;
    } else {
        return AWS_LIBCRYPTO_1_1_1;
    }
#endif
    /*
     * Follow the default path instead of prioritizing the compiled version. This works, since we enforce that
     * the compiled version and runtime version must be the same for BoringSSL and AWS-LC in
     * `s_validate_libcrypto_linkage()`.
     */
    return AWS_LIBCRYPTO_NONE;
}

/* Given libcrypto version, return the filename of the .so */
static char *s_libcrypto_sharedlib_filename(enum aws_libcrypto_version version) {
    switch (version) {
        case AWS_LIBCRYPTO_1_0_2:
            return "libcrypto.so.1.0.0";
        case AWS_LIBCRYPTO_1_1_1:
            return "libcrypto.so.1.1";
        default:
            return "libcrypto.so";
    }
}

static bool s_load_libcrypto_sharedlib(enum aws_libcrypto_version version) {
    const char *libcrypto_version = s_libcrypto_sharedlib_filename(version);

    AWS_LOGF_DEBUG(AWS_LS_CAL_LIBCRYPTO_RESOLVE, "loading %s", libcrypto_version);
    void *module = dlopen(libcrypto_version, RTLD_NOW);
    if (module) {
        AWS_LOGF_DEBUG(AWS_LS_CAL_LIBCRYPTO_RESOLVE, "resolving against %s", libcrypto_version);
        enum aws_libcrypto_version result = s_resolve_libcrypto_symbols(version, module);
        if (result == version) {
            return true;
        }
        dlclose(module);
    } else {
        AWS_LOGF_DEBUG(AWS_LS_CAL_LIBCRYPTO_RESOLVE, "%s not found", libcrypto_version);
    }

    return false;
}

static enum aws_libcrypto_version s_resolve_libcrypto_sharedlib(void) {
    /* First try to load the same version as the compiled libcrypto version */
    const enum aws_libcrypto_version compiled_version = s_libcrypto_version_at_compile_time();
    if (compiled_version != AWS_LIBCRYPTO_NONE) {
        if (s_load_libcrypto_sharedlib(compiled_version)) {
            return compiled_version;
        }
    }

    /* If compiled_version is AWS_LIBCRYPTO_1_1_1, we have already tried to load it and failed. So, skip it here. */
    if (compiled_version != AWS_LIBCRYPTO_1_1_1) {
        if (s_load_libcrypto_sharedlib(AWS_LIBCRYPTO_1_1_1)) {
            return AWS_LIBCRYPTO_1_1_1;
        }
    }

    /* If compiled_version is AWS_LIBCRYPTO_1_0_2, we have already tried to load it and failed. So, skip it here. */
    if (compiled_version != AWS_LIBCRYPTO_1_0_2) {
        if (s_load_libcrypto_sharedlib(AWS_LIBCRYPTO_1_0_2)) {
            return AWS_LIBCRYPTO_1_0_2;
        }
    }

    AWS_LOGF_DEBUG(AWS_LS_CAL_LIBCRYPTO_RESOLVE, "loading libcrypto.so");
    void *module = dlopen("libcrypto.so", RTLD_NOW);
    if (module) {
        unsigned long (*openssl_version_num)(void) = NULL;
        *(void **)(&openssl_version_num) = dlsym(module, "OpenSSL_version_num");
        if (openssl_version_num) {
            unsigned long version = openssl_version_num();
            AWS_LOGF_DEBUG(AWS_LS_CAL_LIBCRYPTO_RESOLVE, "libcrypto.so reported version is 0x%lx", version);
            enum aws_libcrypto_version result = AWS_LIBCRYPTO_NONE;
            if (version >= 0x10101000L) {
                AWS_LOGF_DEBUG(AWS_LS_CAL_LIBCRYPTO_RESOLVE, "probing libcrypto.so for aws-lc symbols");
                result = s_resolve_libcrypto_symbols(AWS_LIBCRYPTO_LC, module);
                if (result == AWS_LIBCRYPTO_NONE) {
                    AWS_LOGF_DEBUG(AWS_LS_CAL_LIBCRYPTO_RESOLVE, "probing libcrypto.so for 1.1.1 symbols");
                    result = s_resolve_libcrypto_symbols(AWS_LIBCRYPTO_1_1_1, module);
                }
            } else if (version >= 0x10002000L) {
                AWS_LOGF_DEBUG(AWS_LS_CAL_LIBCRYPTO_RESOLVE, "probing libcrypto.so for 1.0.2 symbols");
                result = s_resolve_libcrypto_symbols(AWS_LIBCRYPTO_1_0_2, module);
            } else {
                AWS_LOGF_DEBUG(AWS_LS_CAL_LIBCRYPTO_RESOLVE, "libcrypto.so reported version is unsupported");
            }
            if (result != AWS_LIBCRYPTO_NONE) {
                return result;
            }
        } else {
            AWS_LOGF_DEBUG(AWS_LS_CAL_LIBCRYPTO_RESOLVE, "Unable to determine version of libcrypto.so");
        }
        dlclose(module);
    } else {
        AWS_LOGF_DEBUG(AWS_LS_CAL_LIBCRYPTO_RESOLVE, "libcrypto.so not found");
    }

    return AWS_LIBCRYPTO_NONE;
}

/* Validate at runtime that we're linked against the same libcrypto we compiled against. */
static void s_validate_libcrypto_linkage(void) {
    /* NOTE: the choice of stack buffer size is somewhat arbitrary. it's
     * possible, but unlikely, that libcrypto version strings may exceed this in
     * the future. we guard against buffer overflow by limiting write size in
     * snprintf with the size of the buffer itself. if libcrypto version strings
     * do eventually exceed the chosen size, this runtime check will fail and
     * will need to be addressed by increasing buffer size.*/
    char expected_version[64] = {0};
#if defined(OPENSSL_IS_AWSLC)
    /* get FIPS mode at runtime because headers don't give any indication of
     * AWS-LC's FIPSness at aws-c-cal compile time. version number can still be
     * captured at preprocess/compile time from AWSLC_VERSION_NUMBER_STRING.*/
    const char *mode = FIPS_mode() ? "AWS-LC FIPS" : "AWS-LC";
    snprintf(expected_version, sizeof(expected_version), "%s %s", mode, AWSLC_VERSION_NUMBER_STRING);
#elif defined(OPENSSL_IS_BORINGSSL)
    snprintf(expected_version, sizeof(expected_version), "BoringSSL");
#elif defined(OPENSSL_IS_OPENSSL)
    snprintf(expected_version, sizeof(expected_version), OPENSSL_VERSION_TEXT);
#elif !defined(BYO_CRYPTO)
#    error Unsupported libcrypto!
#endif
    const char *runtime_version = SSLeay_version(SSLEAY_VERSION);
    AWS_LOGF_DEBUG(
        AWS_LS_CAL_LIBCRYPTO_RESOLVE,
        "Compiled with libcrypto %s, linked to libcrypto %s",
        expected_version,
        runtime_version);
#if defined(OPENSSL_IS_OPENSSL)
    /* Validate that the string "AWS-LC" doesn't appear in OpenSSL version str. */
    AWS_FATAL_ASSERT(strstr("AWS-LC", expected_version) == NULL);
    AWS_FATAL_ASSERT(strstr("AWS-LC", runtime_version) == NULL);
    /* Validate both expected and runtime versions begin with OpenSSL's version str prefix. */
    const char *openssl_prefix = "OpenSSL ";
    AWS_FATAL_ASSERT(strncmp(openssl_prefix, expected_version, strlen(openssl_prefix)) == 0);
    AWS_FATAL_ASSERT(strncmp(openssl_prefix, runtime_version, strlen(openssl_prefix)) == 0);
#else
    AWS_FATAL_ASSERT(strcmp(expected_version, runtime_version) == 0 && "libcrypto mislink");
#endif
}

static enum aws_libcrypto_version s_resolve_libcrypto(void) {
    /* Try to auto-resolve against what's linked in/process space */
    AWS_LOGF_DEBUG(AWS_LS_CAL_LIBCRYPTO_RESOLVE, "searching process and loaded modules");
    enum aws_libcrypto_version result = s_resolve_libcrypto_symbols(AWS_LIBCRYPTO_LC, NULL);
    if (result == AWS_LIBCRYPTO_NONE) {
        AWS_LOGF_DEBUG(AWS_LS_CAL_LIBCRYPTO_RESOLVE, "did not find aws-lc symbols linked");
        result = s_resolve_libcrypto_symbols(AWS_LIBCRYPTO_BORINGSSL, NULL);
    }
    if (result == AWS_LIBCRYPTO_NONE) {
        AWS_LOGF_DEBUG(AWS_LS_CAL_LIBCRYPTO_RESOLVE, "did not find boringssl symbols linked");
        result = s_resolve_libcrypto_symbols(AWS_LIBCRYPTO_1_1_1, NULL);
    }
    if (result == AWS_LIBCRYPTO_NONE) {
        AWS_LOGF_DEBUG(AWS_LS_CAL_LIBCRYPTO_RESOLVE, "did not find libcrypto 1.1.1 symbols linked");
        result = s_resolve_libcrypto_symbols(AWS_LIBCRYPTO_1_0_2, NULL);
    }

    if (result == AWS_LIBCRYPTO_NONE) {
        AWS_LOGF_DEBUG(AWS_LS_CAL_LIBCRYPTO_RESOLVE, "did not find libcrypto 1.0.2 symbols linked");
        AWS_LOGF_DEBUG(
            AWS_LS_CAL_LIBCRYPTO_RESOLVE,
            "libcrypto symbols were not linked, searching for shared libraries and loading it");
        result = s_resolve_libcrypto_sharedlib();
    }

    s_validate_libcrypto_linkage();

    return result;
}

/* Ignore warnings about how CRYPTO_get_locking_callback() always returns NULL on 1.1.1 */
#if !defined(__GNUC__) || (__GNUC__ * 100 + __GNUC_MINOR__ * 10 > 410)
#    pragma GCC diagnostic push
#    pragma GCC diagnostic ignored "-Waddress"
#endif

/* Openssl 1.0.x requires special handling for its locking callbacks or else it's not thread safe */
#if !defined(OPENSSL_IS_AWSLC) && !defined(OPENSSL_IS_BORINGSSL)
static struct aws_mutex *s_libcrypto_locks = NULL;

static void s_locking_fn(int mode, int n, const char *unused0, int unused1) {
    (void)unused0;
    (void)unused1;

    if (mode & CRYPTO_LOCK) {
        aws_mutex_lock(&s_libcrypto_locks[n]);
    } else {
        aws_mutex_unlock(&s_libcrypto_locks[n]);
    }
}

static unsigned long s_id_fn(void) {
    return (unsigned long)aws_thread_current_thread_id();
}
#endif

void aws_cal_platform_init(struct aws_allocator *allocator) {
    int version = s_resolve_libcrypto();
    AWS_FATAL_ASSERT(version != AWS_LIBCRYPTO_NONE && "libcrypto could not be resolved");
    AWS_FATAL_ASSERT(g_aws_openssl_evp_md_ctx_table);
    AWS_FATAL_ASSERT(g_aws_openssl_hmac_ctx_table);

    s_libcrypto_allocator = allocator;

#if !defined(OPENSSL_IS_AWSLC) && !defined(OPENSSL_IS_BORINGSSL)
    /* Ensure that libcrypto 1.0.2 has working locking mechanisms. This code is macro'ed
     * by libcrypto to be a no-op on 1.1.1 */
    if (!CRYPTO_get_locking_callback()) {
        /* on 1.1.1 this is a no-op */
        CRYPTO_set_locking_callback(s_locking_fn);
        if (CRYPTO_get_locking_callback() == s_locking_fn) {
            s_libcrypto_locks = aws_mem_acquire(allocator, sizeof(struct aws_mutex) * CRYPTO_num_locks());
            AWS_FATAL_ASSERT(s_libcrypto_locks);
            size_t lock_count = (size_t)CRYPTO_num_locks();
            for (size_t i = 0; i < lock_count; ++i) {
                aws_mutex_init(&s_libcrypto_locks[i]);
            }
        }
    }

    if (!CRYPTO_get_id_callback()) {
        CRYPTO_set_id_callback(s_id_fn);
    }
#endif
}

/*
 * Shutdown any resources before unloading CRT (ex. dlclose).
 * This is currently aws-lc specific.
 * Ex. why we need it:
 * aws-lc uses thread local data extensively and registers thread atexit
 * callback to clean it up.
 * there are cases where crt gets dlopen'ed and then dlclose'ed within a larger program
 * (ex. nodejs workers).
 * with glibc, dlclose actually removes symbols from global space (musl does not).
 * once crt is unloaded, thread atexit will no longer point at a valid aws-lc
 * symbol and will happily crash when thread is closed.
 * AWSLC_thread_local_shutdown was added by aws-lc to let teams remove thread
 * local data manually before lib is unloaded.
 * We can't call AWSLC_thread_local_shutdown in cal cleanup because it renders
 * aws-lc unusable and there is no way to reinitilize aws-lc to a working state,
 * i.e. everything that depends on aws-lc stops working after shutdown (ex. curl).
 * So instead rely on GCC/Clang destructor extension to shutdown right before
 * crt gets unloaded. Does not work on msvc, but thats a bridge we can cross at
 * a later date (since we dont support aws-lc on win right now)
 * TODO: do already init'ed check on lc similar to what we do for s2n, so we
 * only shutdown when we initialized aws-lc. currently not possible because
 * there is no way to check that aws-lc has been initialized.
 */
void __attribute__((destructor)) s_cal_crypto_shutdown(void) {
#if defined(OPENSSL_IS_AWSLC)
    AWSLC_thread_local_shutdown();
#endif
}

void aws_cal_platform_clean_up(void) {
#if !defined(OPENSSL_IS_AWSLC) && !defined(OPENSSL_IS_BORINGSSL)
    if (CRYPTO_get_locking_callback() == s_locking_fn) {
        CRYPTO_set_locking_callback(NULL);
        size_t lock_count = (size_t)CRYPTO_num_locks();
        for (size_t i = 0; i < lock_count; ++i) {
            aws_mutex_clean_up(&s_libcrypto_locks[i]);
        }
        aws_mem_release(s_libcrypto_allocator, s_libcrypto_locks);
    }

    if (CRYPTO_get_id_callback() == s_id_fn) {
        CRYPTO_set_id_callback(NULL);
    }
#endif

#if defined(OPENSSL_IS_AWSLC)
    AWSLC_thread_local_clear();
#endif

    s_libcrypto_allocator = NULL;
}

void aws_cal_platform_thread_clean_up(void) {
#if defined(OPENSSL_IS_AWSLC)
    AWSLC_thread_local_clear();
#endif
}

#if !defined(__GNUC__) || (__GNUC__ >= 4 && __GNUC_MINOR__ > 1)
#    pragma GCC diagnostic pop
#endif
